﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Mvc;

namespace MVCms.Core.PluginProvider
{
    public class CustomViewEngine : WebFormViewEngine
    {
        #region Plugin Support
        /// <summary>
        /// Initializes a new instance of the <see cref="CustomViewEngine"/> class.
        /// </summary>
        /// <param name="viewLocations">The view locations.</param>
        public CustomViewEngine(string[] viewLocations): base()
        {
            string[] tempArray = new string[ViewLocationFormats.Length + viewLocations.Length];
            ViewLocationFormats.CopyTo(tempArray, 0);

            for (int i = 0; i < viewLocations.Length; i++)
            {
                tempArray[ViewLocationFormats.Length + i] = viewLocations[i];
            }

            ViewLocationFormats = tempArray;
            PartialViewLocationFormats = ViewLocationFormats;
            MasterLocationFormats = new string[] { "~/Content/{2}/Site.master" }; //theme support
        }

        /// <summary>
        /// Determines whether [is app resource path] [the specified virtual path].
        /// </summary>
        /// <param name="virtualPath">The virtual path.</param>
        /// <returns>
        /// 	<c>true</c> if [is app resource path] [the specified virtual path]; otherwise, <c>false</c>.
        /// </returns>
        private bool IsAppResourcePath(string virtualPath)
        {
            String checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
            return checkPath.StartsWith("~/Plugins/", StringComparison.InvariantCultureIgnoreCase);
        }

        /// <summary>
        /// Determines whether a file at the specified location exists for the specified controller context.
        /// </summary>
        /// <param name="controllerContext">The controller context.</param>
        /// <param name="virtualPath">The virtual path.</param>
        /// <returns>
        /// true if the file exists; otherwise, false.
        /// </returns>
        protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
        {
            if (IsAppResourcePath(virtualPath))
            {
                return System.Web.Hosting.HostingEnvironment.VirtualPathProvider.FileExists(virtualPath);
            }
            else
                return base.FileExists(controllerContext, virtualPath);
        }

        /// <summary>
        /// Gets the view engines of all installed plugins.
        /// </summary>
        /// <returns>List<CustomViewEngine></returns>
        public static List<CustomViewEngine> GetViewEngines()
        {
            var viewEngines = new List<CustomViewEngine>();
            var directoryInfo = new DirectoryInfo(HttpRuntime.BinDirectory);

            // loop through dll's in bin directory
            foreach (var fileInfo in directoryInfo.GetFiles())
            {
                // check if it's a plugin
                if (!fileInfo.Name.StartsWith("Plugin"))
                {
                    continue;
                }
                //make sure it's the dll
                if (!fileInfo.Name.EndsWith(".dll"))
                {
                    continue;
                }

                //read the assembly into memory
                var assemblyBytes = File.ReadAllBytes(fileInfo.FullName);
                var assembly = Assembly.Load(assemblyBytes);
                
                //extracts raw view pages
                var rawViews = assembly.GetManifestResourceNames();

                var views = new List<string>();
                
                // clean up view and extract the actual view
                foreach (var view in rawViews)
                {
                    var viewArr = view.Split('.');
                    viewArr[viewArr.Length - 2] = "{0}";
                    views.Add(string.Join(".", viewArr));
                }

                // add the view to the view engines
                viewEngines.AddRange(
                    views.Select(view => new CustomViewEngine(new[] { string.Format("~/Plugins/{0}/{1}", assembly.ManifestModule, view) })));
            }

            return viewEngines;
        }
        #endregion

        #region Theme Support
        /// <summary>
        /// Finds the specified view by using the specified controller context and master view name.
        /// </summary>
        /// <param name="controllerContext">The controller context.</param>
        /// <param name="viewName">The name of the view.</param>
        /// <param name="masterName">The name of the master view.</param>
        /// <param name="useCache">true to use the cached view.</param>
        /// <returns>The page view.</returns>
        /// <exception cref="T:System.ArgumentNullException">The <paramref name="controllerContext"/> parameter is null (Nothing in Visual Basic).</exception>
        /// <exception cref="T:System.ArgumentException">The <paramref name="viewName"/> parameter is null or empty.</exception>
        public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
        {
            if (controllerContext == null)
                throw new ArgumentNullException("controllerContext");

            if (string.IsNullOrEmpty(viewName))
                throw new ArgumentException("Value is required.", "viewName");

            string themeName = this.GetThemeToUse(controllerContext);
            string[] searchedViewLocations;
            string[] searchedMasterLocations;
            string controllerName = controllerContext.RouteData.GetRequiredString("controller");
            string viewPath = this.GetViewPath(this.ViewLocationFormats, viewName, controllerName, out searchedViewLocations);
            string masterPath = this.GetMasterPath(this.MasterLocationFormats, viewName, controllerName, themeName, out searchedMasterLocations);

            if (!(string.IsNullOrEmpty(viewPath)) && (masterPath != string.Empty || string.IsNullOrEmpty(masterName)))
                return new ViewEngineResult((this.CreateView(controllerContext, viewPath, masterPath)), this);

            return new ViewEngineResult(searchedViewLocations.Union<string>(searchedMasterLocations));
        }

        /// <summary>
        /// Finds the specified partial view by using the specified controller context.
        /// </summary>
        /// <param name="controllerContext">The controller context.</param>
        /// <param name="partialViewName">The name of the partial view.</param>
        /// <param name="useCache">true to use the cached partial view.</param>
        /// <returns>The partial view.</returns>
        /// <exception cref="T:System.ArgumentNullException">The <paramref name="controllerContext"/> parameter is null (Nothing in Visual Basic).</exception>
        /// <exception cref="T:System.ArgumentException">The <paramref name="partialViewName"/> parameter is null or empty.</exception>
        public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
        {
            if (controllerContext == null)
                throw new ArgumentNullException("controllerContext");

            if (string.IsNullOrEmpty(partialViewName))
                throw new ArgumentException("Value is required.", partialViewName);

            string themeName = this.GetThemeToUse(controllerContext);
            string[] searchedLocations;
            string controllerName = controllerContext.RouteData.GetRequiredString("controller");
            string partialPath = this.GetViewPath(this.PartialViewLocationFormats, partialViewName, controllerName, out searchedLocations);

            if (string.IsNullOrEmpty(partialPath))
                return new ViewEngineResult(searchedLocations);


            return new ViewEngineResult(this.CreatePartialView(controllerContext, partialPath), this);
        }

        /// <summary>
        /// Gets the theme to use.
        /// </summary>
        /// <param name="controllerContext">The controller context.</param>
        /// <returns></returns>
        private string GetThemeToUse(ControllerContext controllerContext)
        {
            if (controllerContext.HttpContext.Request.QueryString.AllKeys.Contains("theme"))
            {
                string themeName = controllerContext.HttpContext.Request.QueryString["theme"];
                controllerContext.HttpContext.Session.Add("Theme", themeName);
            }
            else if (controllerContext.HttpContext.Session["Theme"] == null)
            {
                controllerContext.HttpContext.Session.Add("Theme", "Default");
            }

            return controllerContext.HttpContext.Session["Theme"].ToString();
        }

        /// <summary>
        /// Gets the view path.
        /// </summary>
        /// <param name="locations">The locations.</param>
        /// <param name="viewName">Name of the view.</param>
        /// <param name="controllerName">Name of the controller.</param>
        /// <param name="searchedLocations">The searched locations.</param>
        /// <returns></returns>
        private string GetViewPath(string[] locations, string viewName, string controllerName, out string[] searchedLocations)
        {
            string path = null;
            searchedLocations = new string[locations.Length];

            for (int i = 0; i < locations.Length; i++)
            {
                path = string.Format(CultureInfo.InvariantCulture, locations[i], new object[] { viewName, controllerName });
                if (this.VirtualPathProvider.FileExists(path))
                {
                    searchedLocations = new string[0];
                    return path;
                }
                searchedLocations[i] = path;
            }
            return null;
        }

        /// <summary>
        /// Gets the master path.
        /// </summary>
        /// <param name="locations">The locations.</param>
        /// <param name="viewName">Name of the view.</param>
        /// <param name="controllerName">Name of the controller.</param>
        /// <param name="themeName">Name of the theme.</param>
        /// <param name="searchedLocations">The searched locations.</param>
        /// <returns></returns>
        private string GetMasterPath(string[] locations, string viewName, string controllerName, string themeName, out string[] searchedLocations)
        {
            string path = null;
            searchedLocations = new string[locations.Length];

            for (int i = 0; i < locations.Length; i++)
            {
                path = string.Format(CultureInfo.InvariantCulture, locations[i], new object[] { viewName, controllerName, themeName });
                if (this.VirtualPathProvider.FileExists(path))
                {
                    searchedLocations = new string[0];
                    return path;
                }
                searchedLocations[i] = path;
            }
            return null;
        }
        #endregion
    }
}